package com.immediatus.engine;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Vibrator;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import com.immediatus.contracts.IUpdateHandler;
import com.immediatus.engine.contracts.IAccelerometerListener;
import com.immediatus.engine.contracts.ILocationListener;
import com.immediatus.engine.contracts.IOrientationListener;
import com.immediatus.engine.contracts.ITouchController;
import com.immediatus.engine.input.MultiTouchController;
import com.immediatus.engine.input.TouchEvent;
import com.immediatus.engine.input.sensor.SensorDelay;
import com.immediatus.engine.input.sensor.accelerometer.AccelerometerData;
import com.immediatus.engine.input.sensor.location.LocationProviderStatus;
import com.immediatus.engine.input.sensor.orientation.OrientationData;
import com.immediatus.engine.layer.Canvas;
import com.immediatus.engine.util.RunnableHandler;
import com.immediatus.graphics.camera.Camera;
import com.immediatus.graphics.factories.FontFactory;
import com.immediatus.graphics.managers.BufferObjectManager;
import com.immediatus.graphics.managers.FontManager;
import com.immediatus.graphics.managers.TextureManager;
import com.immediatus.graphics.util.OpenGLWrapper;
import com.immediatus.options.AccelerometerSensorOptions;
import com.immediatus.options.EngineOptions;
import com.immediatus.options.LocationSensorOptions;
import com.immediatus.options.OrientationSensorOptions;
import com.immediatus.util.UpdateHandlerList;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;


public class Engine implements SensorEventListener, OnTouchListener, ITouchController.ITouchEventCallback, LocationListener{
    private static final SensorDelay SENSORDELAY_DEFAULT = SensorDelay.GAME;
    private static final int UPDATEHANDLERS_CAPACITY_DEFAULT = 32;
    private static final long NANOSECONDSPERSECOND = 1000000000;

    private final Camera _camera;
    private Canvas _canvas;

    private int _canvasWidth = 1;
    private int _canvasHeight = 1;

    private final TextureManager _textureManager = new TextureManager();
    private final BufferObjectManager _bufferObjectManager = new BufferObjectManager();
    private final FontManager _fontManager = new FontManager();

    private ITouchController _touchController;
    private ILocationListener _locationListener;
    private IAccelerometerListener _accelerometerListener;
    private IOrientationListener _orientationListener;

    private Vibrator _vibrator;
    private Location _location;
    private AccelerometerData _accelerometerData;
    private OrientationData _orientationData;

    private final EngineOptions _engineOptions;
    private final State _threadLocker = new State();

    private boolean _running = false;
    private long _lastTick = -1;
    private float _secondsElapsedTotal = 0;

    private final UpdateThread _updateThread = new UpdateThread();
    private final RunnableHandler _updateThreadRunnableHandler = new RunnableHandler();
    private final UpdateHandlerList _updateHandlers = new UpdateHandlerList(UPDATEHANDLERS_CAPACITY_DEFAULT);


    public Engine(final EngineOptions options_){
        FontFactory.reset();
        BufferObjectManager.setActiveInstance(this._bufferObjectManager);

        this._engineOptions = options_;
        this.setTouchController(new MultiTouchController());
        this._camera = options_.getCamera();

        this._updateThread.start();
    }

    public boolean isRunning(){
        return this._running;
    }

    public synchronized void start(){
        if (!this._running){
            this._lastTick = System.nanoTime();
            this._running = true;
        }
    }

    public synchronized void stop(){
        if (this._running){
            this._running = false;
        }
    }

    public Canvas getCanvas(){
        return this._canvas;
    }

    public void setCanvas(final Canvas canvas_){
        this._canvas = canvas_;
    }

    public EngineOptions getEngineOptions(){
        return this._engineOptions;
    }

    public Camera getCamera(){
        return this._camera;
    }

    public float getSecondsElapsedTotal(){
        return this._secondsElapsedTotal;
    }

    public void setCanvasSize(final int w_, final int h_){
        this._canvasWidth = w_;
        this._canvasHeight = h_;
        this.onUpdateCameraSurface();
    }

    protected void onUpdateCameraSurface(){
        this._camera.setSurfaceSize(0, 0, this._canvasWidth, this._canvasHeight);
    }

    public int getCanvasWidth(){
        return this._canvasWidth;
    }

    public int getCanvsHeight(){
        return this._canvasHeight;
    }

    public ITouchController getTouchController(){
        return this._touchController;
    }

    public void setTouchController(final ITouchController controller_){
        this._touchController = controller_;
        this._touchController.applyTouchOptions(this._engineOptions.getTouchOptions());
        this._touchController.setTouchEventCallback(this);
    }

    public AccelerometerData getAccelerometerData(){
        return this._accelerometerData;
    }

    public OrientationData getOrientationData(){
        return this._orientationData;
    }

    public TextureManager getTextureManager(){
        return this._textureManager;
    }

    public FontManager getFontManager(){
        return this._fontManager;
    }

    public void clearUpdateHandlers(){
        this._updateHandlers.clear();
    }

    public void registerUpdateHandler(final IUpdateHandler handler_){
        this._updateHandlers.add(handler_);
    }

    public void unregisterUpdateHandler(final IUpdateHandler handler_){
        this._updateHandlers.remove(handler_);
    }

    @Override
    public void onAccuracyChanged(final Sensor sensor_, final int accuracy_){
        if (this._running){
            switch (sensor_.getType()){
                case Sensor.TYPE_ACCELEROMETER:
                    if (this._accelerometerData != null){
                        this._accelerometerData.setAccuracy(accuracy_);
                        this._accelerometerListener.onAccelerometerChanged(this._accelerometerData);
                    } else if (this._orientationData != null){
                        this._orientationData.setAccelerometerAccuracy(accuracy_);
                        this._orientationListener.onOrientationChanged(this._orientationData);
                    }
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:
                    this._orientationData.setMagneticFieldAccuracy(accuracy_);
                    this._orientationListener.onOrientationChanged(this._orientationData);
                    break;
            }
        }
    }

    @Override
    public void onSensorChanged(final SensorEvent event_){
        if (this._running){
            switch (event_.sensor.getType()){
                case Sensor.TYPE_ACCELEROMETER:
                    if (this._accelerometerData != null){
                        this._accelerometerData.setValues(event_.values);
                        this._accelerometerListener.onAccelerometerChanged(this._accelerometerData);
                    } else if (this._orientationData != null){
                        this._orientationData.setAccelerometerValues(event_.values);
                        this._orientationListener.onOrientationChanged(this._orientationData);
                    }
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:
                    this._orientationData.setMagneticFieldValues(event_.values);
                    this._orientationListener.onOrientationChanged(this._orientationData);
                    break;
            }
        }
    }

    @Override
    public void onLocationChanged(final Location location_){
        if (this._location == null){
            this._location = location_;
        } else{
            if (location_ == null){
                this._locationListener.onLocationLost();
            } else{
                this._location = location_;
                this._locationListener.onLocationChanged(location_);
            }
        }
    }

    @Override
    public void onProviderDisabled(final String provider_){
        this._locationListener.onLocationProviderDisabled();
    }

    @Override
    public void onProviderEnabled(final String provider_){
        this._locationListener.onLocationProviderEnabled();
    }

    @Override
    public void onStatusChanged(final String provider_, final int status_, final Bundle extras_){
        switch (status_){
            case LocationProvider.AVAILABLE:
                this._locationListener.onLocationProviderStatusChanged(LocationProviderStatus.AVAILABLE, extras_);
                break;
            case LocationProvider.OUT_OF_SERVICE:
                this._locationListener.onLocationProviderStatusChanged(LocationProviderStatus.OUT_OF_SERVICE, extras_);
                break;
            case LocationProvider.TEMPORARILY_UNAVAILABLE:
                this._locationListener.onLocationProviderStatusChanged(LocationProviderStatus.TEMPORARILY_UNAVAILABLE, extras_);
                break;
        }
    }

    @Override
    public boolean onTouch(final View view_, final MotionEvent event_){
        if (this._running){
            final boolean handled = this._touchController.onHandleMotionEvent(event_);
            try{
                Thread.sleep(20);
            } catch (final InterruptedException e){
            }
            return handled;
        } else{
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(final TouchEvent touchEvent_){
        final Canvas canvas = this.getLayerFromCanvasTouchEvent();
        final Camera camera = this.getCameraFromCanvasTouchEvent();

        this.convertSurfaceToLayerTouchEvent(camera, touchEvent_);
        return this.onTouchCanvas(canvas, touchEvent_);
    }

    protected boolean onTouchCanvas(final Canvas canvas_, final TouchEvent event_){
        return canvas_ != null && canvas_.onTouchEvent(event_);
    }

    public void runOnUpdateThread(final Runnable runnable_){
        this._updateThreadRunnableHandler.postRunnable(runnable_);
    }

    public void interruptUpdateThread(){
        this._updateThread.interrupt();
    }

    public void onResume(){
        this._textureManager.reloadTextures();
        this._fontManager.reloadFonts();
        BufferObjectManager.setActiveInstance(this._bufferObjectManager);
        this._bufferObjectManager.reloadBufferObjects();
    }

    public void onPause(){
    }

    protected Camera getCameraFromCanvasTouchEvent(){
        return this.getCamera();
    }

    protected Canvas getLayerFromCanvasTouchEvent(){
        return this._canvas;
    }

    protected void convertSurfaceToLayerTouchEvent(final Camera camera_, final TouchEvent event_){
        camera_.convertSurfaceToLayerTouchEvent(event_, this._canvasWidth, this._canvasHeight);
    }

    public void onLoadComplete(final Canvas canvas_){
        this.setCanvas(canvas_);
    }

    void onTickUpdate() throws InterruptedException{
        if (this._running){
            final long secondsElapsed = this.getNanosecondsElapsed();
            this.onUpdate(secondsElapsed);
            this.yieldDraw();
        } else{
            this.yieldDraw();
            Thread.sleep(16);
        }
    }

    private void yieldDraw() throws InterruptedException{
        final State threadLocker = this._threadLocker;
        threadLocker.notifyCanDraw();
        threadLocker.waitUntilCanUpdate();
    }

    protected void onUpdate(final long nanosecondsElapsed_) throws InterruptedException{
        final float pSecondsElapsed = (float) nanosecondsElapsed_ / NANOSECONDSPERSECOND;

        this._secondsElapsedTotal += pSecondsElapsed;
        this._lastTick += nanosecondsElapsed_;

        this._touchController.onUpdate(pSecondsElapsed);
        this.updateUpdateHandlers(pSecondsElapsed);
        this.onUpdateCanvas(pSecondsElapsed);
    }

    protected void onUpdateCanvas(final float secondsElapsed_){
        if (this._canvas != null){
            this._canvas.onUpdate(secondsElapsed_);
        }
    }

    protected void updateUpdateHandlers(final float secondsElapsed_){
        this._updateThreadRunnableHandler.onUpdate(secondsElapsed_);
        this._updateHandlers.onUpdate(secondsElapsed_);
        this.getCamera().onUpdate(secondsElapsed_);
    }

    public void onDrawFrame(final GL10 gl_) throws InterruptedException{
        final State threadLocker = this._threadLocker;

        threadLocker.waitUntilCanDraw();

        this._textureManager.updateTextures(gl_);
        this._fontManager.updateFonts(gl_);
        if (OpenGLWrapper.EXTENSIONS_VERTEXBUFFEROBJECTS)
            this._bufferObjectManager.updateBufferObjects((GL11) gl_);
        this.onDrawCanvas(gl_);

        threadLocker.notifyCanUpdate();
    }

    protected void onDrawCanvas(final GL10 gl_){
        final Camera camera = this.getCamera();
        if (this._canvas != null){
            this._canvas.onDraw(gl_, camera);
        }
    }

    private long getNanosecondsElapsed(){
        final long now = System.nanoTime();
        return this.calculateNanosecondsElapsed(now, this._lastTick);
    }

    protected long calculateNanosecondsElapsed(final long now_, final long lastTick_){
        return now_ - lastTick_;
    }

    public boolean enableVibrator(final Context context_){
        this._vibrator = (Vibrator) context_.getSystemService(Context.VIBRATOR_SERVICE);
        return this._vibrator != null;
    }

    public void vibrate(final long milliseconds_) throws IllegalStateException{
        if (this._vibrator != null){
            this._vibrator.vibrate(milliseconds_);
        } else{
            throw new IllegalStateException("Vibro is disable.");
        }
    }

    public void vibrate(final long[] pattern_, final int repeat) throws IllegalStateException{
        if (this._vibrator != null){
            this._vibrator.vibrate(pattern_, repeat);
        } else{
            throw new IllegalStateException("Vibro is disable.");
        }
    }

    public void enableLocationSensor(final Context context_, final ILocationListener listener_, final LocationSensorOptions options_){
        this._locationListener = listener_;
        final LocationManager locationManager = (LocationManager) context_.getSystemService(Context.LOCATION_SERVICE);
        final String locationProvider = locationManager.getBestProvider(options_, options_.isEnabledOnly());
        locationManager.requestLocationUpdates(locationProvider, options_.getMinimumTriggerTime(), options_.getMinimumTriggerDistance(), this);
        this.onLocationChanged(locationManager.getLastKnownLocation(locationProvider));
    }

    public void disableLocationSensor(final Context context_){
        final LocationManager locationManager = (LocationManager) context_.getSystemService(Context.LOCATION_SERVICE);
        locationManager.removeUpdates(this);
    }

    public boolean enableAccelerometerSensor(final Context context_, final IAccelerometerListener listener_){
        return this.enableAccelerometerSensor(context_, listener_, new AccelerometerSensorOptions(SENSORDELAY_DEFAULT));
    }

    public boolean enableAccelerometerSensor(final Context context_, final IAccelerometerListener listener_, final AccelerometerSensorOptions options_){
        final SensorManager sensorManager = (SensorManager) context_.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER)){
            this._accelerometerListener = listener_;

            if (this._accelerometerData == null){
                final Display display = ((WindowManager) context_.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
                final int displayRotation = display.getOrientation();
                this._accelerometerData = new AccelerometerData(displayRotation);
            }

            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER, options_.getSensorDelay());

            return true;
        } else{
            return false;
        }
    }

    public boolean disableAccelerometerSensor(final Context context_){
        final SensorManager sensorManager = (SensorManager) context_.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER)){
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER);
            return true;
        } else{
            return false;
        }
    }

    public boolean enableOrientationSensor(final Context context_, final IOrientationListener listener_){
        return this.enableOrientationSensor(context_, listener_, new OrientationSensorOptions(SENSORDELAY_DEFAULT));
    }

    public boolean enableOrientationSensor(final Context context_, final IOrientationListener listener_, final OrientationSensorOptions options_){
        final SensorManager sensorManager = (SensorManager) context_.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER) &&
                this.isSensorSupported(sensorManager, Sensor.TYPE_MAGNETIC_FIELD)){
            this._orientationListener = listener_;

            if (this._orientationData == null){
                final Display display = ((WindowManager) context_.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
                final int displayRotation = display.getOrientation();
                this._orientationData = new OrientationData(displayRotation);
            }

            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER, options_.getSensorDelay());
            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_MAGNETIC_FIELD, options_.getSensorDelay());

            return true;
        } else{
            return false;
        }
    }

    public boolean disableOrientationSensor(final Context context_){
        final SensorManager sensorManager = (SensorManager) context_.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER) &&
                this.isSensorSupported(sensorManager, Sensor.TYPE_MAGNETIC_FIELD)){
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER);
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_MAGNETIC_FIELD);
            return true;
        } else{
            return false;
        }
    }

    private boolean isSensorSupported(final SensorManager manager_, final int type_){
        return manager_.getSensorList(type_).size() > 0;
    }

    private void registerSelfAsSensorListener(final SensorManager manager_, final int type_, final SensorDelay delay_){
        final Sensor sensor = manager_.getSensorList(type_).get(0);
        manager_.registerListener(this, sensor, delay_.getDelay());
    }

    private void unregisterSelfAsSensorListener(final SensorManager manager_, final int type_){
        final Sensor sensor = manager_.getSensorList(type_).get(0);
        manager_.unregisterListener(this, sensor);
    }

    private class UpdateThread extends Thread{
        public UpdateThread(){
            super("UpdateThread");
        }

        @Override
        public void run(){
            android.os.Process.setThreadPriority(Engine.this._engineOptions.getUpdateThreadPriority());
            try{
                while (true){
                    Engine.this.onTickUpdate();
                }
            } catch (final InterruptedException e){
                this.interrupt();
            }
        }
    }

    private static class State{
        boolean _drawing = false;

        public synchronized void notifyCanDraw(){
            this._drawing = true;
            this.notifyAll();
        }

        public synchronized void notifyCanUpdate(){
            this._drawing = false;
            this.notifyAll();
        }

        public synchronized void waitUntilCanDraw() throws InterruptedException{
            while (!this._drawing){
                this.wait();
            }
        }

        public synchronized void waitUntilCanUpdate() throws InterruptedException{
            while (this._drawing){
                this.wait();
            }
        }
    }
}
